from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.deps import get_current_user, get_db
from app.models.diary import DiaryEntry
from app.models.diary_comment import DiaryComment
from app.models.user import User
from app.models.memory_node import MemoryNode
from app.models.memory_edge import MemoryEdge
from app.models.memory_node import MemoryNode
from app.models.memory_edge import MemoryEdge
from app.schemas import diary as diary_schema
from app.api.response import success_response
from app.services.langchain_service import langchain_service
from app.services.notification_manager import notification_manager
from app.services.relationship_utils import get_partner_email
from app.db.session import async_session_factory
import re
import structlog

logger = structlog.get_logger('love_notes.diary')

router = APIRouter(redirect_slashes=False)


def _entry_accessible(entry: DiaryEntry | None, user: User) -> bool:
  if not entry:
    return False
  if entry.user_id == user.id:
    return True
  if user.active_relationship_id and entry.relationship_id == user.active_relationship_id:
    return True
  return False




@router.get('')
async def list_diary_entries(
  limit: int = 50,
  offset: int = 0,
  current_user: User = Depends(get_current_user),
  session: AsyncSession = Depends(get_db),
) -> dict:
  filters = [DiaryEntry.deleted_at.is_(None)]
  if current_user.active_relationship_id:
    filters.append(
      or_(
        DiaryEntry.user_id == current_user.id,
        DiaryEntry.relationship_id == current_user.active_relationship_id,
      )
    )
  else:
    filters.append(DiaryEntry.user_id == current_user.id)

  safe_limit = max(1, min(limit, 100))
  safe_offset = max(0, offset)
  query = (
    select(DiaryEntry)
    .where(*filters)
    .order_by(DiaryEntry.created_at.desc())
    .offset(safe_offset)
    .limit(safe_limit)
  )
  result = await session.scalars(query)
  entries = result.all()
  payload = [
    diary_schema.DiaryEntryResponse.model_validate(entry, from_attributes=True).model_dump(mode='json')
    for entry in entries
  ]
  return success_response({
    'items': payload,
    'next_offset': safe_offset + len(payload) if len(payload) == safe_limit else None,
  })


@router.get('/{entry_id}')
async def get_diary_entry(
  entry_id: int,
  comment_limit: int = 20,
  comment_offset: int = 0,
  current_user: User = Depends(get_current_user),
  session: AsyncSession = Depends(get_db),
) -> dict:
  entry = await session.get(DiaryEntry, entry_id)
  if not _entry_accessible(entry, current_user):
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Diary not found')

  safe_limit = max(1, min(comment_limit, 100))
  safe_offset = max(0, comment_offset)

  comments_stmt = (
    select(DiaryComment)
    .where(DiaryComment.diary_entry_id == entry.id)
    .order_by(DiaryComment.created_at.asc())
    .offset(safe_offset)
    .limit(safe_limit)
  )
  comments_query = await session.scalars(comments_stmt)
  comments = comments_query.all()

  total_comments = await session.scalar(
    select(func.count()).select_from(DiaryComment).where(DiaryComment.diary_entry_id == entry.id)
  )

  entry_payload = diary_schema.DiaryEntryResponse.model_validate(entry, from_attributes=True).model_dump(mode='json')
  comments_payload = []
  for comment in comments:
    comment_data = diary_schema.DiaryCommentResponse.model_validate(
      comment, from_attributes=True
    ).model_dump(mode='json')
    comment_data['author_name'] = comment.user.username if comment.user else None
    comments_payload.append(comment_data)

  return success_response(
    {
      **entry_payload,
      'comments': comments_payload,
      'comments_meta': {
        'next_offset': safe_offset + len(comments_payload) if len(comments_payload) == safe_limit else None,
        'total': total_comments,
      },
    }
  )


@router.get('/{entry_id}/comments')
async def list_diary_comments(
  entry_id: int,
  limit: int = 20,
  offset: int = 0,
  current_user: User = Depends(get_current_user),
  session: AsyncSession = Depends(get_db),
) -> dict:
  entry = await session.get(DiaryEntry, entry_id)
  if not _entry_accessible(entry, current_user):
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Diary not found')

  safe_limit = max(1, min(limit, 100))
  safe_offset = max(0, offset)

  comments_stmt = (
    select(DiaryComment)
    .where(DiaryComment.diary_entry_id == entry.id)
    .order_by(DiaryComment.created_at.asc())
    .offset(safe_offset)
    .limit(safe_limit)
  )
  comments_query = await session.scalars(comments_stmt)
  comments = comments_query.all()

  payload = []
  for comment in comments:
    comment_data = diary_schema.DiaryCommentResponse.model_validate(
      comment, from_attributes=True
    ).model_dump(mode='json')
    comment_data['author_name'] = comment.user.username if comment.user else None
    payload.append(comment_data)

  next_offset = safe_offset + len(payload) if len(payload) == safe_limit else None
  return success_response({
    'items': payload,
    'next_offset': next_offset,
  })


@router.post('', status_code=status.HTTP_201_CREATED)
async def create_diary_entry(
  payload: diary_schema.DiaryEntryCreate,
  current_user: User = Depends(get_current_user),
  session: AsyncSession = Depends(get_db),
  background: BackgroundTasks = None,
) -> dict:
  entry = DiaryEntry(
    user_id=current_user.id,
    relationship_id=current_user.active_relationship_id,
    title=payload.title or None,
    content_md=payload.content_md,
    mood=payload.mood,
    tags_json=payload.tags,
    visibility=payload.visibility,
  )
  session.add(entry)
  await session.commit()
  await session.refresh(entry)

  # 异步后处理：AI 摘要/情绪与标签生成、通知邮件发送
  async def _postprocess_diary(entry_id: int, user_id: int, allow_ai: bool, notify: bool):
    async with async_session_factory() as s:
      try:
        db_entry = await s.get(DiaryEntry, entry_id)
        db_user = await s.get(User, user_id)
        if not db_entry or not db_user:
          return
        # 结构化提取心情与标签
        if allow_ai:
          extracted = await langchain_service.extract_mood_and_tags(content=db_entry.content_md)
          if extracted:
            if extracted.get('mood') and not db_entry.mood:
              db_entry.mood = extracted['mood']
            if extracted.get('tags'):
              existing = set(db_entry.tags_json or [])
              for t in extracted['tags']:
                if t not in existing:
                  existing.add(t)
              db_entry.tags_json = list(existing)

          # 然后生成摘要（使用提取后的心情/标签增强摘要质量）
          summary = await langchain_service.summarize_diary(
            content=db_entry.content_md,
            mood=db_entry.mood,
            tags=db_entry.tags_json or [],
          )
          if summary:
            db_entry.ai_summary = summary
            # 若缺少标题，使用摘要首句/前几字生成标题
            if not (db_entry.title and db_entry.title.strip()):
              first_line = (summary or '').strip().splitlines()[0] if summary else ''
              # 按句号/顿号/冒号切分取第一段
              import re as _re
              candidate = _re.split(r'[。！？!?:：\-~]|\s{2,}', first_line)[0].strip()
              if not candidate:
                candidate = (db_entry.content_md or '').strip()[:24]
              db_entry.title = (candidate[:24] or '无题')
          elif not (db_entry.title and db_entry.title.strip()):
            # 未启用/失败时的兜底标题
            fallback = (db_entry.content_md or '').strip().splitlines()[0][:24]
            db_entry.title = fallback or '无题'
        s.add(db_entry)
        await s.commit()

        # 从日记内容中抽取“重要记忆”，仅当有有效信息时才写入（不写噪声）
        try:
          # 将日记作为“用户陈述”输入记忆提取器（直接写图谱，不再写 JSON）
          messages = [{'role': 'user', 'content': db_entry.content_md or ''}]
          merged_mems = await langchain_service.extract_and_merge_user_memories(previous={}, messages=messages)
          if merged_mems:
            # 同步到图谱（只处理提醒与话题，轻量同步）
            async def _upsert_node(ntype: str, label: str, props: dict | None = None) -> int:
              props = props or {}
              node = await s.scalar(
                select(MemoryNode).where(
                  MemoryNode.user_id == db_user.id,
                  MemoryNode.relationship_id == db_user.active_relationship_id,
                  MemoryNode.type == ntype,
                  MemoryNode.label == label,
                )
              )
              if not node:
                node = MemoryNode(
                  user_id=db_user.id,
                  relationship_id=db_user.active_relationship_id,
                  type=ntype,
                  label=label,
                  props_json=props,
                )
              else:
                base = dict(node.props_json or {})
                base.update(props)
                node.props_json = base
              s.add(node)
              await s.flush()
              return node.id

            async def _upsert_edge(src_id: int, dst_id: int, etype: str, props: dict | None = None) -> None:
              props = props or {}
              edge = await s.scalar(
                select(MemoryEdge).where(
                  MemoryEdge.user_id == db_user.id,
                  MemoryEdge.relationship_id == db_user.active_relationship_id,
                  MemoryEdge.type == etype,
                  MemoryEdge.src_id == src_id,
                  MemoryEdge.dst_id == dst_id,
                )
              )
              if not edge:
                edge = MemoryEdge(
                  user_id=db_user.id,
                  relationship_id=db_user.active_relationship_id,
                  type=etype,
                  src_id=src_id,
                  dst_id=dst_id,
                  props_json=props,
                )
              else:
                base = dict(edge.props_json or {})
                base.update(props)
                edge.props_json = base
              s.add(edge)

            person_id = await _upsert_node('person', '我', {})
            partner_id = None
            if db_user.active_relationship_id:
              partner_id = await _upsert_node('person', '伴侣', {})
            mem = merged_mems or {}
            # 话题（若抽取模型填充 notes 或相关词，可按需扩展；此处仅示例 topics 来源于标签）
            for t in (db_entry.tags_json or []):
              label = str(t).strip()
              if not label:
                continue
              tid = await _upsert_node('topic', label, {})
              await _upsert_edge(person_id, tid, 'mentions', {})
            # 提醒
            from uuid import uuid4 as _uuid4
            for r in (mem.get('reminders') or []):
              if not isinstance(r, dict):
                continue
              txt = str(r.get('text') or '').strip()
              if not txt:
                continue
              rid = await _upsert_node('reminder', txt, {
                'id': r.get('id') or _uuid4().hex,
                'due': r.get('due'),
                'tz': r.get('tz'),
                'status': r.get('status') or 'active',
              })
              await _upsert_edge(person_id, rid, 'reminds', {})
            await s.commit()
        except Exception:
          logger.warning('diary memory extraction skipped', entry_id=entry_id)

        # 通知邮件
        if notify:
          try:
            partner_email = await get_partner_email(s, db_user)
            if partner_email:
              await notification_manager.notify_diary_created(
                partner_email=partner_email,
                author_name=db_user.username,
                title=db_entry.title,
                summary=db_entry.ai_summary,
              )
          except Exception:
            logger.warning('Failed to send diary notification (non-blocking)', entry_id=entry_id)
      except Exception:
        logger.exception('Postprocess diary failed', entry_id=entry_id)

  if background is not None:
    background.add_task(_postprocess_diary, entry.id, current_user.id, payload.allow_ai_summary, payload.notify_partner)
  else:
    # 兼容无 BackgroundTasks 注入的场景，降级为 fire-and-forget
    import asyncio
    asyncio.create_task(_postprocess_diary(entry.id, current_user.id, payload.allow_ai_summary, payload.notify_partner))

  response = diary_schema.DiaryEntryResponse.model_validate(entry, from_attributes=True)
  return success_response(response.model_dump(mode='json'))


@router.put('/{entry_id}')
async def update_diary_entry(
  entry_id: int,
  payload: diary_schema.DiaryEntryUpdate,
  current_user: User = Depends(get_current_user),
  session: AsyncSession = Depends(get_db),
) -> dict:
  entry = await session.get(DiaryEntry, entry_id)
  if not _entry_accessible(entry, current_user):
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Diary not found')

  for field, value in payload.model_dump(exclude_unset=True).items():
    if field == 'tags':
      entry.tags_json = value
    else:
      setattr(entry, field, value)

  session.add(entry)
  await session.commit()
  await session.refresh(entry)
  response = diary_schema.DiaryEntryResponse.model_validate(entry, from_attributes=True)
  return success_response(response.model_dump(mode='json'))


@router.delete('/{entry_id}')
async def delete_diary_entry(
  entry_id: int,
  current_user: User = Depends(get_current_user),
  session: AsyncSession = Depends(get_db),
) -> dict:
  entry = await session.get(DiaryEntry, entry_id)
  if not _entry_accessible(entry, current_user):
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Diary not found')
  entry.deleted_at = datetime.now(tz=timezone.utc)
  session.add(entry)
  await session.commit()
  return success_response()


@router.post('/{entry_id}/ai-summarize')
async def summarize_diary_entry(
  entry_id: int,
  current_user: User = Depends(get_current_user),
  session: AsyncSession = Depends(get_db),
) -> dict:
  entry = await session.get(DiaryEntry, entry_id)
  if not _entry_accessible(entry, current_user):
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Diary not found')

  summary = await langchain_service.summarize_diary(
    content=entry.content_md, mood=entry.mood, tags=entry.tags_json or []
  )
  if summary:
    entry.ai_summary = summary
    session.add(entry)
    await session.commit()
    await session.refresh(entry)
  response = diary_schema.DiaryEntryResponse.model_validate(entry, from_attributes=True)
  return success_response(response.model_dump(mode='json'))


@router.post('/{entry_id}/comments', status_code=status.HTTP_201_CREATED)
async def create_diary_comment(
  entry_id: int,
  payload: diary_schema.DiaryCommentCreate,
  current_user: User = Depends(get_current_user),
  session: AsyncSession = Depends(get_db),
) -> dict:
  entry = await session.get(DiaryEntry, entry_id)
  if not _entry_accessible(entry, current_user):
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Diary not found')

  comment = DiaryComment(
    diary_entry_id=entry.id,
    user_id=current_user.id,
    content=payload.content,
    mood=payload.mood,
  )
  session.add(comment)
  await session.commit()
  await session.refresh(comment)

  response = diary_schema.DiaryCommentResponse.model_validate(
    comment, from_attributes=True, context={'author': current_user.username}
  )
  payload_out = response.model_dump(mode='json')
  payload_out['author_name'] = current_user.username
  return success_response(payload_out)


@router.delete('/{entry_id}/comments/{comment_id}')
async def delete_diary_comment(
  entry_id: int,
  comment_id: int,
  current_user: User = Depends(get_current_user),
  session: AsyncSession = Depends(get_db),
) -> dict:
  entry = await session.get(DiaryEntry, entry_id)
  if not _entry_accessible(entry, current_user):
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Diary not found')

  comment = await session.get(DiaryComment, comment_id)
  if not comment or comment.diary_entry_id != entry.id:
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Comment not found')
  if comment.user_id != current_user.id and entry.user_id != current_user.id:
    raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='No permission to delete comment')

  await session.delete(comment)
  await session.commit()
  return success_response(None)
